package com.module.file.service.impl;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.Channel;
import java.nio.channels.FileChannel;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletResponse;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.FileSystemUtils;

import com.base.commom.utils.FileSizeUtil;
import com.module.file.param.MultipartFileParam;
import com.module.file.service.StorageService;
import com.module.file.utils.Constants;
import com.module.file.utils.FileMD5Util;

/**
 * Created by 超文 on 2017/5/2.
 */
@Service
public class StorageServiceImpl implements StorageService {

    private final Logger logger = LoggerFactory.getLogger(StorageServiceImpl.class);
    // 保存文件的根目录
    private Path rootPath;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    //这个必须与前端设定的值一致
    @Value("${breakpoint.upload.chunkSize}")
    private long CHUNK_SIZE;

    @Value("${breakpoint.upload.dir}")
    private String finalDirPath;

    @Autowired
    public StorageServiceImpl(@Value("${breakpoint.upload.dir}") String location) {
        this.rootPath = Paths.get(location);
    }

    @Override
    public void deleteAll() {
        logger.info("开发初始化清理数据，start");
        FileSystemUtils.deleteRecursively(rootPath.toFile());
        stringRedisTemplate.delete(Constants.FILE_UPLOAD_STATUS);
        stringRedisTemplate.delete(Constants.FILE_MD5_KEY);
        logger.info("开发初始化清理数据，end");
    }

    @Override
    public void init() {
        try {
            Files.createDirectory(rootPath);
        } catch (FileAlreadyExistsException e) {
            logger.error("文件夹已经存在了，不用再创建。");
        } catch (IOException e) {
            logger.error("初始化root文件夹失败。", e);
        }
    }

    @Override
    public void uploadFileRandomAccessFile(MultipartFileParam param) throws IOException {
        String fileName = param.getName();
        String tempDirPath = finalDirPath + param.getMd5();
        String tempFileName = fileName + "_tmp";
        File tmpDir = new File(tempDirPath);
        File tmpFile = new File(tempDirPath, tempFileName);
        if (!tmpDir.exists()) {
            tmpDir.mkdirs();
        }

        RandomAccessFile accessTmpFile = new RandomAccessFile(tmpFile, "rw");
        long offset = CHUNK_SIZE * param.getChunk();
        //定位到该分片的偏移量
        accessTmpFile.seek(offset);
        //写入该分片数据
        accessTmpFile.write(param.getFile().getBytes());
        // 释放
        accessTmpFile.close();

        boolean isOk = checkAndSetUploadProgress(param, tempDirPath);
        if (isOk) {
            boolean flag = renameFile(tmpFile, fileName);
            System.out.println("upload complete !!" + flag + " name=" + fileName);
        }
    }

    @Override
    public void uploadFileByMappedByteBuffer(MultipartFileParam param) throws IOException {
        String fileName = param.getName();
        String uploadDirPath = finalDirPath + param.getMd5();
        String tempFileName = fileName + "_tmp";
        File tmpDir = new File(uploadDirPath);
        File tmpFile = new File(uploadDirPath, tempFileName);
        if (!tmpDir.exists()) {
            tmpDir.mkdirs();
        }

        @SuppressWarnings("resource")
		RandomAccessFile tempRaf = new RandomAccessFile(tmpFile, "rw");
        FileChannel fileChannel = tempRaf.getChannel();

        //写入该分片数据
        long offset = CHUNK_SIZE * param.getChunk();
        byte[] fileData = param.getFile().getBytes();
        MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, offset, fileData.length);
        mappedByteBuffer.put(fileData);
        // 释放
        FileMD5Util.freedMappedByteBuffer(mappedByteBuffer);
        fileChannel.close();

        boolean isOk = checkAndSetUploadProgress(param, uploadDirPath);
        if (isOk) {
            boolean flag = renameFile(tmpFile, fileName);
            System.out.println("upload complete !!" + flag + " name=" + fileName);
        }
    }

    /**
     * 检查并修改文件上传进度
     *
     * @param param
     * @param uploadDirPath
     * @return
     * @throws IOException
     */
    private boolean checkAndSetUploadProgress(MultipartFileParam param, String uploadDirPath) throws IOException {
        String fileName = param.getName();
        File confFile = new File(uploadDirPath, fileName + ".conf");
        RandomAccessFile accessConfFile = new RandomAccessFile(confFile, "rw");
        //把该分段标记为 true 表示完成
        System.out.println("set part " + param.getChunk() + " complete");
        accessConfFile.setLength(param.getChunks());
        accessConfFile.seek(param.getChunk());
        accessConfFile.write(Byte.MAX_VALUE);

        //completeList 检查是否全部完成,如果数组里是否全部都是(全部分片都成功上传)
        byte[] completeList = FileUtils.readFileToByteArray(confFile);
        byte isComplete = Byte.MAX_VALUE;
        for (int i = 0; i < completeList.length && isComplete == Byte.MAX_VALUE; i++) {
            //与运算, 如果有部分没有完成则 isComplete 不是 Byte.MAX_VALUE
            isComplete = (byte) (isComplete & completeList[i]);
            System.out.println("check part " + i + " complete?:" + completeList[i]);
        }

        accessConfFile.close();
        if (isComplete == Byte.MAX_VALUE) {
            stringRedisTemplate.opsForHash().put(Constants.FILE_UPLOAD_STATUS, param.getMd5(), "true");
            stringRedisTemplate.opsForValue().set(Constants.FILE_MD5_KEY + param.getMd5(), uploadDirPath + "/" + fileName);
            return true;
        } else {
            if (!stringRedisTemplate.opsForHash().hasKey(Constants.FILE_UPLOAD_STATUS, param.getMd5())) {
                stringRedisTemplate.opsForHash().put(Constants.FILE_UPLOAD_STATUS, param.getMd5(), "false");
            }
            if (stringRedisTemplate.hasKey(Constants.FILE_MD5_KEY + param.getMd5())) {
                stringRedisTemplate.opsForValue().set(Constants.FILE_MD5_KEY + param.getMd5(), uploadDirPath + "/" + fileName + ".conf");
            }
            return false;
        }
    }

    /**
     * 文件重命名
     *
     * @param toBeRenamed   将要修改名字的文件
     * @param toFileNewName 新的名字
     * @return
     */
    public boolean renameFile(File toBeRenamed, String toFileNewName) {
        //检查要重命名的文件是否存在，是否是文件
        if (!toBeRenamed.exists() || toBeRenamed.isDirectory()) {
            logger.info("File does not exist: " + toBeRenamed.getName());
            return false;
        }
        String p = toBeRenamed.getParent();
        File newFile = new File(p + File.separatorChar + toFileNewName);
        //修改文件名
        return toBeRenamed.renameTo(newFile);
    }

    private File file;
    /**
     * 下载文件
     * @author chenluqiang
     * @date 2019年3月14日 上午11:07:53 
     *	@param response
     *	@param fileName
     * @return 
     * @throws IOException 
     */
	@Override
	@Cacheable(cacheNames="file",keyGenerator = "keyGenerator",cacheManager="redisCacheManager", unless="#result==null")	
	public HttpServletResponse downloadFileByMappedByteBuffer(HttpServletResponse response, String fileName) throws IOException {
		 // 设置编码
    	response.setCharacterEncoding("UTF-8");
        String filePath = rootPath + "/"+fileName;
       file = new File(filePath);
        // 判断文件是否存在
        if (file.exists()) {
        	//进行文件分片
        	this.splitFile();
        	this.merge(response);
        }else{
            response.sendError(HttpServletResponse.SC_NOT_FOUND, "找不到相关资源");
        }
        return response;
	}
	
	private int count = 10;
	/**
	 * 将文件分片
	 * @author chenluqiang
	 * @date 2019年3月14日 下午5:05:09 
	 * @throws IOException
	 */
	private void splitFile() throws IOException {
		//获取目标文件 预分配文件所占的空间 在磁盘中创建一个指定大小的文件   r 是只读
		RandomAccessFile raf = new RandomAccessFile(file, "r");
        long length = raf.length();//文件的总长度
        long maxSize = length / count;//文件切片后的长度
        long offSet = 0L;//初始化偏移量
        //申明文件切割后的文件磁盘
        for (int i = 0; i < count - 1; i++) { //最后一片单独处理
            long begin = offSet;
            long end = (i + 1) * maxSize;
//            offSet = writeFile(file, begin, end, i);
            offSet = this.getWrite(i, begin, end);
        }
        if (length - offSet > 0) {
            this.getWrite(count-1, offSet, length);
        }
	}
	
	/**
     * 指定文件每一份的边界，写入不同文件中
     * @param file 源文件
     * @param index 源文件的顺序标识
     * @param begin 开始指针的位置
     * @param end 结束指针的位置
     * @return long
     */
    public long getWrite(int index,long begin,long end){
        long endPointer = 0L;
        try {
        	RandomAccessFile in = new RandomAccessFile(file, "r");
        	//定义一个可读，可写的文件并且后缀名为.tmp的二进制文件
            File tempFile = new File(file.getPath() + "_" + index + ".tmp");
            RandomAccessFile out = new RandomAccessFile(tempFile, "rw");
            /*in.seek(begin);
            //读取分片数据
            FileChannel inChannel = in.getChannel();  
            MappedByteBuffer inBuff = inChannel.map(FileChannel.MapMode.READ_ONLY,0,end-begin);  
            //写入该分片数据
            FileChannel outChannel = out.getChannel();
//            MappedByteBuffer outBuff = outChannel.map(FileChannel.MapMode.READ_WRITE, 0 ,end-begin);
            //复制
//            outChannel.transferFrom(inChannel,0, inChannel.size());
//            inChannel.transferTo(begin, end, outChannel);
//            outBuff.put(inBuff);
            outChannel.write(inBuff);
            inBuff.clear();
//            outBuff.clear();
            //定义当前读取文件的指针	
//            endPointer = in.getFilePointer();
            // 释放
           FileMD5Util.freedMappedByteBuffer(inBuff);
            inChannel.close();
            
              FileMD5Util.freedMappedByteBuffer(outBuff);
            outChannel.close();*/
            
            //关闭输入流
           /* in.close();
            //关闭输出流
            out.close();*/
            //申明具体每一文件的字节数组
            byte[] b = new byte[1024];
            int n = 0;
            //从指定位置读取文件字节流
            in.seek(begin);
            //判断文件流读取的边界
            while(in.getFilePointer() <= end && (n = in.read(b)) != -1){
                //从指定每一份文件的范围，写入不同的文件
                out.write(b, 0, n);
            }
            //定义当前读取文件的指针
            endPointer = in.getFilePointer();
            //关闭输入流
            in.close();
            //关闭输出流
            out.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return endPointer;
    }
    /**
     * 文件合并
     * @param tempFile 分割前的文件名
     * @param tempCount 文件个数
     */
	public void merge(HttpServletResponse response) {
//        RandomAccessFile raf = null;
        BufferedOutputStream bos =null;
        try {
            //申明随机读取文件RandomAccessFile
//            raf = new RandomAccessFile(new File(file), "rw");
        	response.reset();
        	// 设置response的Header，注意这句，如果开启，默认浏览器会进行下载操作，如果注释掉，浏览器会默认预览。
            /*response.setContentType("application/octet-stream");
             * response.addHeader("Content-Disposition",
             "attachment;filename=" + file.getName());
               response.addHeader("Content-Length", "" + file.length());
             */
//        	视频播放器
        	response.setContentType("video/mpeg");
        	response.setContentLength((int) file.length());
             
        	bos = new BufferedOutputStream(response.getOutputStream());
            //开始合并文件，对应切片的二进制文件
        	FileChannel readChannel = null;
        	 MappedByteBuffer outBuff = null;
            for (int i = 0; i < count; i++) {
            	//读取切片文件
        		RandomAccessFile reader = new RandomAccessFile(new File(file.getPath() + "_" + i + ".tmp"), "r");
        		readChannel = reader.getChannel();
                outBuff = readChannel.map(FileChannel.MapMode.READ_ONLY, 0 ,readChannel.size());
                
               while(outBuff.hasRemaining()) {
            	   byte b =  outBuff.get();
                   bos.write(b);
               }
               bos.flush();
               /* ByteBuffer buff = outBuff.get(b);*/
//                byte[] b = new byte[2048];
              /*  ByteBuffer buff = ByteBuffer.allocateDirect(1024);  
                int n;
                while ((n=readChannel.read(buff)) != -1) {//读
                	bos.write(n);//写
                }
                bos.flush();*/
                /*
                int n = 0;
                //先读后写
                while ((n = reader.read(b)) != -1) {//读
                	bos.write(b, 0, n);//写
                }*/
            }
            // 释放
            FileMD5Util.freedMappedByteBuffer(outBuff);
            readChannel.close();
            logger.info("————————文件下载完成————————————");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                bos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

	/**
	 * 获取文件列表
	 * @author chenluqiang
	 * @date 2019年3月14日 下午2:52:19
	 */
    
	@Override
	public List<Map<String,Object>> listFile() {
		List<Map<String,Object>> result = new ArrayList<>();
		List<String> keys = new ArrayList<>();
		//获取文件md5
		Map<Object, Object> allMap = stringRedisTemplate.opsForHash().entries(Constants.FILE_UPLOAD_STATUS);
		for(Object key:allMap.keySet()) {
			if(key!=null&&StringUtils.isNotBlank(key.toString())
					&&allMap.get(key.toString())!=null
					&&StringUtils.isNotBlank(allMap.get(key.toString()).toString())
					&&allMap.get(key.toString()).equals("true")) {
				keys.add(key.toString());
			}
		}
		//获取文件的路径
		for(String md5 : keys) {
			Map<String,Object> temp = new HashMap<String,Object>();
			String filepath = stringRedisTemplate.opsForValue().get(Constants.FILE_MD5_KEY+md5);
			File file = new File(filepath);
			if(file.exists()) {
				temp.put("filepath", file.getPath());
				temp.put("md5", md5);
				temp.put("filename", file.getName());
				temp.put("length",FileSizeUtil.formetFileSize(file.length(), 3));
//				temp.put("lastModify", file.lastModified());
				result.add(temp);
			}
		}
		logger.info("result:"+result);
		return result;
	}
}
